package com.zzh.zzhaiagent.websocket.service.impl;

import cn.hutool.core.lang.UUID;
import com.github.benmanes.caffeine.cache.Cache;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.zzh.zzhaiagent.utils.JwtUtil;
import com.zzh.zzhaiagent.websocket.adapter.WebSocketAdapter;
import com.zzh.zzhaiagent.websocket.domain.dto.WsChannelExtraDTO;
import com.zzh.zzhaiagent.websocket.domain.dto.WxInfo;
import com.zzh.zzhaiagent.websocket.domain.vo.response.*;
import com.zzh.zzhaiagent.websocket.service.WebSocketService;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import jakarta.annotation.Resource;
import me.chanjar.weixin.common.bean.WxOAuth2UserInfo;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import static com.zzh.zzhaiagent.constant.RedisKeyConstant.REFRESH_TOKEN_KEY_PREFIX;

/**
 * @author: zzh
 * @date: 2024/12/18 14:45:14
 * @version: 1.0
 */
@Service
public class WebSocketServiceImpl  implements WebSocketService {

    /**
     * 开发包自带的微信服务接口，封装好了所有微信相关的请求操作
     * 微信公众号开发包：weixin-java-mp
     */
    @Resource
    private WxMpService wxMpService;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private ApplicationEventPublisher applicationEventPublisher;

    @Resource
    @Qualifier("websocketExecutor")
    private ThreadPoolTaskExecutor wsExecutor;


    //ConcurrentHashMap 实现高并发 ！线程安全 ！
    /**
     * 存放所有在线用户的 Channel 和相关信息的 Map
     * 登录信息 - Channel
     */
    private static final ConcurrentHashMap<Channel, WsChannelExtraDTO> ONLINE_WS_USER_MAP = new ConcurrentHashMap<>();


    /**
     * 存储uuid 与 openID(用户微信信息) 映射关系
     */
    private static final ConcurrentHashMap<String , String> OPEN_ID_UUID_MAP = new ConcurrentHashMap<>();
    
    
    /**
     * 存储uuid 与 (用户微信信息) 映射关系
     */
    private static final ConcurrentHashMap<String , WxInfo> UUID_INFO_MAP = new ConcurrentHashMap<>();
    
    public static WxInfo getWxInfoByUUID(String uuid) {
        return UUID_INFO_MAP.get(uuid);
    }

    /**
     * 临时保存登录 code - Channel 映射Map
     * maximumSize = 10000 个存储空间
     * 带有过期时间的缓存 防止内存泄漏
     */
    public static final Duration CODE_EXPIRE_TIME = Duration.ofHours(1);
    private static final Cache<Integer, Channel> WAIT_CODE_CHANNEL_MAP =  Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(CODE_EXPIRE_TIME)
            .build();

    /**
     * openId - code 映射Map
     * 方便点击授权登录的时候，我们可以openId拿到code->channel的映射关系
     * @param channel
     */
    private static final ConcurrentHashMap<String, Integer> OPEN_ID_CODE_MAP = new ConcurrentHashMap<>();
    @Override
    public void saveOpenIdCode(String openId, Integer code) {
        OPEN_ID_CODE_MAP.put(openId, code);
    }
    @Override
    public Integer getCodeByOpenId(String openId) {
        //从openId 获取 code -> channel的映射关系
        return OPEN_ID_CODE_MAP.get(openId);
    }

    @Override
    public void scanLoginSuccess(Integer codeByOpenId, Long uid) {
        // 获取channel并且移除code
        Channel channel = WAIT_CODE_CHANNEL_MAP.getIfPresent(codeByOpenId);
        if (ObjectUtil.isNull(channel)){
            return;
        }
        //生成token，并返回给前端
        //生成accessToken 和 refreshToken
        String accessToken = JwtUtil.createJWT(String.valueOf(uid));
        String refreshToken = JwtUtil.createRefreshToken(String.valueOf(uid));
        //生成一个UUID作为refreshToken的key，存储到redis中
        String uuid = UUID.randomUUID().toString(false);//不携带-
        //存储refreshToken到redis (不应该用userId作为key，就用accessToken作为key)
        stringRedisTemplate.opsForValue().set(REFRESH_TOKEN_KEY_PREFIX + uuid,refreshToken,7, TimeUnit.DAYS);

        WSBaseResp<WSLoginSuccess> wsLoginSuccessWSBaseResp = WebSocketAdapter.buildLoginSuccessResp(accessToken, uuid);
        // 移除code,防止重复登录 主动移除
        WAIT_CODE_CHANNEL_MAP.invalidate(codeByOpenId);
        sendMsg(channel, wsLoginSuccessWSBaseResp);
    }

    @Override
    public void sendMsgWaitUserClickAuth(Integer loginCode, String uuid, String openId) {
        //绑定openId和loginCode
        saveOpenIdCode(openId, loginCode);

        // 先获取channel
        Channel channel = WAIT_CODE_CHANNEL_MAP.getIfPresent(loginCode);
        if (ObjectUtil.isNull(channel)){
            return;
        }
        // 发送消息 构建消息体
        WSBaseResp<WSLoginWait> wsBaseResp = WebSocketAdapter.buildScanSuccessWait(uuid);
        sendMsg(channel, wsBaseResp);
    }

    /**
     * token认证登录
     * @param channel
     * @param token
     */
    @Override
    public void authorize(Channel channel, String token) {
        //判断是否有效token
//        Long validUid = loginService.getValidUid(token);
        Long validUid = null;
        if (ObjectUtil.isNotNull(validUid)){
            //token有效 登录成功
            WsChannelExtraDTO wsChannelExtraDTO = ONLINE_WS_USER_MAP.get(channel);
            wsChannelExtraDTO.setUserId(validUid);
            ONLINE_WS_USER_MAP.put(channel, wsChannelExtraDTO);
//            // 发送登录成功消息
//            User user = userDao.getById(validUid);
//            // 查询用户角色信息
//            Long power = roleService.findPowerByUid(validUid);
//            WSBaseResp<WSLoginSuccess> wsBaseResp = WebSocketAdapter.buildLoginSuccessResp(user,token,power);
//            sendMsg(channel, wsBaseResp);
        }else {
            //前端token失效
            sendMsg(channel, WebSocketAdapter.buildInvalidTokenResp());
        }
    }

//    // 线程池执行发送消息
//    @Override
//    public void sendBlackMsgToAll(Long uid) {
//        ONLINE_WS_USER_MAP.forEach((channel, wsChannelExtraDTO) -> {
//            wsExecutor.execute(() -> {
//                if (wsChannelExtraDTO.getUserId().equals(uid)){
//                    // 发送消息 您已经被拉黑
//                    sendMsg(channel, WebSocketAdapter.buildBlackYourSelfResp());
//                }else {
//                    // 发送消息 该用户被拉黑
//                    sendMsg(channel, WebSocketAdapter.buildBlackOtherResp());
//                }
//            });
//        });
//    }

    @Override
    public void saveUUIDWxInfoOpenId(String openId, String uuid) {
        // 保存openId和uuid的映射关系
        OPEN_ID_UUID_MAP.put(openId,uuid);
        UUID_INFO_MAP.put(uuid, WxInfo.builder().openId(openId).build());
    }

    /**
     * 保存其他微信信息
     * @param info 用户信息
     */
    @Override
    public void saveUUIDWxInfoOther(WxOAuth2UserInfo info) {
        String openid = info.getOpenid();
        String nickname = info.getNickname();
        String headImgUrl = info.getHeadImgUrl();
        System.out.println("微信授权信息；" + info.toString());
        WxInfo orDefault = UUID_INFO_MAP.getOrDefault(OPEN_ID_UUID_MAP.get(openid), new WxInfo());
        orDefault.setNickname(nickname);
        orDefault.setHeadImgUrl(headImgUrl);
        UUID_INFO_MAP.put(OPEN_ID_UUID_MAP.get(openid), orDefault);
    }

    @Override
    public void verifyEmail(Integer codeByOpenId) {
        //获取channel
        // 获取channel并且移除code
        Channel channel = WAIT_CODE_CHANNEL_MAP.getIfPresent(codeByOpenId);
        if (ObjectUtil.isNull(channel)){
            return;
        }
        // 发送消息 构建消息体
        WSBaseResp wsBaseResp = WebSocketAdapter.buildVerifyEmailResp();
        sendMsg(channel, wsBaseResp);
    }

    /**
     * uuid 删除 loginCode ， openId， uuid 缓存
     * @param openIdUUid uuid
     */
    public static void deleteLoginCodeOpenIdUUIDCache(String openIdUUid) {
        // 获取uuid -> info
        WxInfo wxInfo = getWxInfoByUUID(openIdUUid);
        String openId = wxInfo.getOpenId();
        // 删除openId -> loginCode
        Integer loginCode = OPEN_ID_CODE_MAP.get(openId);
        // 删除openId -> loginCode
        OPEN_ID_CODE_MAP.remove(openId);
        // 删除loginCode -> channel
        WAIT_CODE_CHANNEL_MAP.invalidate(loginCode);
        // 删除openId -> uuid
        OPEN_ID_UUID_MAP.remove(openId);
        // 删除uuid -> info
        UUID_INFO_MAP.remove(openIdUUid);
    }

    @Override
    public void connectChannel(Channel channel) {
        // 先保存空对象，因为连接不代表登录，只是建立连接
        ONLINE_WS_USER_MAP.put(channel, new WsChannelExtraDTO());
    }


    /**
     * 登录 请求二维码信息
     * @param channel
     */
    @Override
    public void handleLoginTicket(Channel channel) throws WxErrorException {
        // 生成随机码
        Integer code = generateRandomCode(channel);

        // 找微信申请拿到登录二维码,临时二维码有效期1小时
        WxMpQrCodeTicket wxMpQrCodeTicket = wxMpService.getQrcodeService().qrCodeCreateTmpTicket(code, (int) CODE_EXPIRE_TIME.getSeconds());

        // 把二维码推送给前端 适配器进行转换 消息返回类型
//        String ticket = wxMpQrCodeTicket.getTicket();
//        String url = wxMpQrCodeTicket.getUrl();
//        WSBaseResp<WSLoginUrl> wsLoginUrlWSBaseResp = new WSBaseResp<>();
//        wsLoginUrlWSBaseResp.setData(new WSLoginUrl(url));
//        wsLoginUrlWSBaseResp.setType(WSReqTypeEnum.LOGIN.getType());
        WSBaseResp<WSLoginUrl> wsBaseResp = WebSocketAdapter.buildLoginUrlResp(wxMpQrCodeTicket);
        sendMsg(channel, wsBaseResp);
    }

    @Override
    public void disconnectChannel(Channel channel) {
        ONLINE_WS_USER_MAP.remove(channel);
        // todo 这里还可以进行用户下线的广播消息！
    }

    // 发送消息
    private void sendMsg(Channel channel, WSBaseResp<?> wsBaseResp) {
        channel.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(wsBaseResp)));
    }

    private Integer generateRandomCode(Channel channel) {
        // 生成随机码 如果随机码已经存在 则putIfAbsent 返回非空对象！不存在则返回null 就跳出do while循环
        // 防止重复生成随机码
        Integer code = null;
        do {
            code = RandomUtil.randomInt(Integer.MAX_VALUE);
        }while (ObjectUtil.isNotNull(WAIT_CODE_CHANNEL_MAP.asMap().putIfAbsent(code,channel)));
        System.out.println("生成随机码：" + code);
        return code;
    }


}
